Add CommandBuffer abstraction with backend-specific downcasting#1033
Conversation
378ec7a to
97e6af6
Compare
| if (!CBOrErr) | ||
| return CBOrErr.takeError(); | ||
| auto CBOwner = std::move(*CBOrErr); | ||
| State.CB = &CBOwner->as<DXCommandBuffer>(); |
There was a problem hiding this comment.
Seems like it would be better if the Invocation state owned the command buffer rather than a temporary object in the function here. If we really should have two copies of it we should probably make it a shared_ptr rather than a unique_ptr.
There was a problem hiding this comment.
I don't think using a shared_ptr for a command buffer is a good idea. The usage of shared_ptr in the rendering backend API is there for resources that can be accessed across multiple threads. However, command buffers should be access from one thread at a time.
I am open for either storing it as a unique_ptr in the InvocationState or just passing it as an argument each time.
There was a problem hiding this comment.
Good points, the InvocationState also lives on the stack here and owns all members, except for the backend-specific-typed command buffer which is merely a pointer reference to another stack member (which becomes dangling when CBOwner is dropped from the stack before InvocationState).
I also vouch for keeping it uniquely owned, that way command buffers can only be passed by reference and not accidentally held and modified in multiple places (trying very hard to forget Rust's natural ownership semantics and page in C++'s nonsensical ones 🙃).
There was a problem hiding this comment.
The only downside of that is needing the backend-specific pointer easily accessible. Either the field is stored twice, or this backend-specific executeProgram() call uses the specific XxxCommandBuffer::create() call to immediately have the right type until every internal field access is replaced with a generic abstraction.
8803d26 to
dadf52d
Compare
dadf52d to
4efc0b0
Compare
095fc23 to
4a67b8e
Compare
66bb416 to
9a1bfe5
Compare
6e5985a to
0d38e7e
Compare
Command buffer creation and management was previously spread across each backend's executeProgram() with no shared interface, making it impossible to manage command buffers from backend-agnostic code. This introduces a CommandBuffer base class on Device so that higher-level code can create and pass around command buffers without knowing the backend. Per-object allocator/pool ownership also prepares for future async execution where multiple command buffers may be in-flight with independent lifetimes. - DX: DXCommandBuffer owns Allocator, CmdList, Fence, Event - VK: VKCommandBuffer owns CmdPool, CmdBuffer; each submission creates a new CommandBuffer for independent lifetime management - MTL: MTLCommandBuffer wraps MTL::CommandBuffer Device::createCommandBuffer() returns Expected<unique_ptr<CommandBuffer>> with a default "not supported" implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0d38e7e to
417e140
Compare
bogner
left a comment
There was a problem hiding this comment.
Is this just so that we can put command buffers from different backends in shared containers, or are you planning to follow up with adding backend agnostic APIs to this interface? Since all of the uses of these types just reach into the object and manipulate the backend specific objects type anyways it isn't really clear to me what this is for.
| // | ||
| // | ||
| //===----------------------------------------------------------------------===// |
There was a problem hiding this comment.
This bit of header comment isn't really necessary if we don't have any description of the module inside of it.
There was a problem hiding this comment.
Ah, I saw it in the other PRs as well and thought that was introduced by clang-tidy as a default. Will remove it or add a description in the next PR.
| template <typename T> T &as() { | ||
| assert(API == T::BackendAPI && "CommandBuffer backend mismatch"); | ||
| return static_cast<T &>(*this); | ||
| } | ||
| template <typename T> const T &as() const { | ||
| assert(API == T::BackendAPI && "CommandBuffer backend mismatch"); | ||
| return static_cast<const T &>(*this); | ||
| } |
There was a problem hiding this comment.
We should probably just set these classes up using LLVM-style RTTI rather than hand rolling it here: https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html
| virtual llvm::Expected<std::unique_ptr<CommandBuffer>> | ||
| createCommandBuffer() = 0; |
There was a problem hiding this comment.
Too late to do anything about it, but note that the commit message claims:
Device::createCommandBuffer() returns Expected<unique_ptr> with a default "not supported" implementation.
However, this is a pure virtual function - there is no default implementation at all.
There was a problem hiding this comment.
You're right - I fixed this last minute after realizing there were not actually any backends that leave this unimplemented, but forgot to update the commit message.
|
|
||
| public: | ||
| explicit CommandBuffer(GPUAPI API) : API(API) {} | ||
| virtual ~CommandBuffer() = default; |
There was a problem hiding this comment.
Classes with vtables should have an anchor to avoid object size and relocation bloat. See https://llvm.org/docs/CodingStandards.html#provide-a-virtual-method-anchor-for-classes-in-headers
|
|
||
| class DXCommandBuffer : public offloadtest::CommandBuffer { | ||
| public: | ||
| static constexpr GPUAPI BackendAPI = GPUAPI::DirectX; |
There was a problem hiding this comment.
Seems like a false positive, but clang is emitting a warning for these:
E:/code/HLSLTest/lib/API/DX/Device.cpp(395,27): error: unused variable 'BackendAPI' [-Werror,-Wunused-const-variable]
395 | static constexpr GPUAPI BackendAPI = GPUAPI::DirectX;
| ^~~~~~~~~~
Apparently we don't have -Werror for the pre-checkin CI jobs here, because it also showed up in those runs: https://github.com/llvm/offload-test-suite/actions/runs/24345003263/job/71083354829#step:11:4368
In any case, switching to LLVM-style RTTI will avoid the issue.
There was a problem hiding this comment.
Correct, this was mostly to have backend-specific downcasting readily available when needed, for special-case tests as well as backend logic like Queue receiving generic CommandBuffer structures.
I'll look at LLVM-style RTTI as well as checking if the CI jobs can be extended with -Werror going forward as the build was always warning-free until this PR 😅
Address post-review comments from @bogner on #1033. These felt significant enough that they weren't worth cramming into an "unrelated" PR but deserve their own clean follow-up in three individual commits. - Add virtual method anchors for all polymorphic types (`Buffer`, `CommandBuffer`, `Fence`, `ImageComparatorBase`, `ImageComparatorDiffImage`, `Texture`) per LLVM coding standards - Add file description to header comment - Switch to LLVM-style RTTI (`classof()`) instead of hand-rolled `as<T>()` + `BackendAPI`, fixing `-Wunused-const-variable` Adding `-Werror` to the build is left for a separate PR as it requires a warning audit first — the project inherits compile flags from LLVM's CMake infrastructure and backend SDK headers may introduce additional warnings. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Command buffer creation and management was previously spread across each backend's
executeProgram()with no shared interface, making it impossible to manage command buffers from backend-agnostic code. This introduces aCommandBufferbase class onDeviceso that higher-level code can create and pass around command buffers without knowing the backend. Per-object allocator/pool ownership also prepares for future async execution where multiple command buffers may be in-flight with independent lifetimes.DXCommandBufferowns Allocator, CmdList, Fence, EventVKCommandBufferowns CmdPool, CmdBuffer; each submission creates a new CommandBuffer for independent lifetime managementMTLCommandBufferwrapsMTL::CommandBufferDevice::createCommandBuffer()returnsExpected<unique_ptr<CommandBuffer>>with a default "not supported" implementation.Test plan
🤖 Generated with Claude Code